feat(plugin/codex): add lifecycle hooks (recall, capture, pre-compact) to codex-memory-plugin#1957
Conversation
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Explore these optional code suggestions:
|
|
Pushed v0.4.0 refactor on top of the original commit. New commits add a End-to-end verified against
Verification SOP at 🤖 Generated with Claude Code |
|
Nice to see this land — env-var-only mode + 1. Honor
|
|
FYI I went ahead and drafted a follow-up commit on top of your branch — couldn't push to your fork directly (403 from It covers all three items above:
README rewritten to document the resolution chain + Verified locally:
Feel free to cherry-pick the single commit into your branch (or pull-rebase from my fork) — happy to also open it as a follow-up PR after this one merges, whichever you prefer. |
Addresses three review points on PR volcengine#1957: 1. Honor OPENVIKING_CLI_CONFIG_FILE for the ovcli.conf override path (matches the convention used by `ov` CLI and claude-code-memory-plugin). OPENVIKING_CONFIG_FILE stays as the ov.conf override; for backward compat it still works when pointed at an ovcli-shaped file. 2. Strict env-first priority for every connection / identity field (baseUrl, apiKey, account, user, agentId). Env vars now win over ovcli.conf, which wins over ov.conf's codex.* block / server.*, which wins over built-in defaults. 3. Unify hook and MCP-server config loading: src/memory-server.ts now imports loadConfig from scripts/config.mjs (relative path stays valid post-compile because servers/ and scripts/ are siblings), eliminating the divergent account/user/agentId fallback chains the PR-Agent reviewer flagged. Auth header: emit Authorization: Bearer (primary, required by OpenViking Cloud) plus the legacy X-API-Key during the transition window. All six fetch sites updated (4 hook scripts + memory-server.ts + compiled servers/memory-server.js). README: document the new resolution chain, OPENVIKING_CLI_CONFIG_FILE, OPENVIKING_BEARER_TOKEN alias, and the Authorization: Bearer migration.
Brings the codex-memory-plugin to feature parity with the claude-code-memory-plugin
by wiring the four Codex lifecycle hooks via `hooks.json`:
- SessionStart -> bootstrap-runtime.mjs (npm ci into ${CODEX_PLUGIN_DATA}/runtime)
- UserPromptSubmit -> auto-recall.mjs (search OV, inject via hookSpecificOutput.additionalContext)
- Stop -> auto-capture.mjs (incremental transcript capture + last_assistant_message commit)
- PreCompact -> pre-compact-capture.mjs (full transcript -> single OV session -> commit)
Differences from the Claude Code plugin baked into the scripts:
- Codex output schema does not allow `decision: "approve"`; no-op is `{}`
- Stop/PreCompact only support `systemMessage`, not `additionalContext`
- Plugin envs are CODEX_PLUGIN_ROOT / CODEX_PLUGIN_DATA
- Config section is `codex` (was `claude_code`); config file defaults to
`~/.openviking/ovcli.conf`, falling back to legacy `~/.openviking/ov.conf`
Other changes:
- src/memory-server.ts now reads ovcli.conf-style configs (top-level `url`,
`api_key`, `account`, `user`, `agent_id`) so the plugin works against
hosted OpenViking deployments out of the box. Env-var-only operation
(OPENVIKING_URL set, no config file) is also supported.
- .mcp.json points at scripts/start-memory-server.mjs, which boots the same
runtime the hooks use, so the MCP path benefits from npm-ci bootstrap.
- README rewritten with architecture diagram, validation SOP, configuration
reference, and a Codex-vs-Claude-Code differences table.
Validated end-to-end against an OpenViking deployment:
- Auto-recall returns ranked memories with full content and emits
hookSpecificOutput.additionalContext.
- Auto-capture (last_assistant_message path) creates a session, commits, and
the OV pipeline extracts events + preferences within ~60s.
- Pre-compact-capture posts a full 4-turn transcript to one OV session,
commits with archived=true, and produces structured leaf memories
(preferences, events, entities) under viking://user/<user>/memories/.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…PreCompact=commit Codex's `Stop` hook fires per turn, not at session end, so committing per-Stop over-fragments memory extraction. And codex re-fires `SessionStart` on short reconnects, so registering an `npm ci` bootstrap there reinstalls the runtime unnecessarily. This change keeps one long-lived OpenViking session per codex `session_id` across all `Stop` invocations, and only triggers the OV memory extractor on `PreCompact` (or via an idle-sweep best-effort commit when codex exits without compacting). - hooks.json: drop SessionStart entry; keep UserPromptSubmit/Stop/PreCompact - scripts/session-state.mjs (new): per-codex-session state under ~/.openviking/codex-plugin-state/, tracks ovSessionId + capturedTurnCount - scripts/auto-capture.mjs (Stop): incremental add_message only, idle-sweep at the tail to commit stale codex sessions (default IDLE_TTL=30 min, override with OPENVIKING_CODEX_IDLE_TTL_MS) - scripts/pre-compact-capture.mjs (PreCompact): catch-up append + commit the long-lived OV session, then null out ovSessionId so the next Stop opens a fresh OV session for the post-compact half - MCP runtime install stays lazy in start-memory-server.mjs (already there); no SessionStart hook means short reconnects don't re-trigger npm ci - VERIFICATION.md: end-to-end SOP against a live OV server (~3 min) - bump plugin to 0.3.0 Verified end-to-end against ov.zaynjarvis.com: Stop adds turns idempotently and incrementally; PreCompact commits to history/archive_001/ with extractor producing memories under viking://user/<user>/memories/profile.md after ~30 s; post-compact Stop opens a fresh OV session; idle-sweep commits stale state files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…lear) commit
Per Zayn's followup ("非必要不要加 idle commit"): drop the idle-sweep added
in the previous commit and use codex's actual context-disappearing signal —
SessionStart with source=clear — to commit orphaned sessions.
Codex hook signal map:
- /compact → PreCompact ✅ commit (already)
- /clear → SessionStart(source=clear) for the NEW session_id;
the prior transcript is orphaned. Now committed.
- /new → SessionStart(source=startup); ambiguous with fresh
codex startup, so we don't act on it.
- /resume / short reconnect → SessionStart(source=resume|startup); no-op
to avoid corrupting still-active sessions.
- SIGTERM/Ctrl+C/exit → no hook fires. Documented as a known gap; users
should /compact before /exit if they want commit.
Changes:
- new scripts/session-start-commit.mjs: gates internally on source=clear,
iterates listStates(), and commits any state file whose codexSessionId
!= the new SessionStart session_id, then clears that state file
- hooks/hooks.json: re-register SessionStart pointing at the new script
(timeout 30s)
- scripts/auto-capture.mjs: remove sweepIdleSessions() and
IDLE_TTL_MS env handling; Stop is now strictly add_message
- README/VERIFICATION.md: update arch diagram, replace idle-sweep step
with SessionStart(source=clear) verify (positive + negative paths),
add "Known gap: SIGTERM/exit are silent" section
- bump to 0.3.1
Verified end-to-end against ov.zaynjarvis.com:
Stop add+idempotent ✓
SessionStart source=startup → {} ✓
SessionStart source=resume → {} ✓
SessionStart source=clear → committed prior OV session, history/archive_001/
appeared, profile.md gained "Favorite snack: dark chocolate" within 30 s.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…r gate) Codex's hooks dispatcher matches the SessionStart hook's `matcher` field against the SessionStart `source` value. Setting matcher to "clear" means codex won't even spawn our script on `source=startup` or `source=resume` (short reconnects); we previously gated this in-script. The internal source check in session-start-commit.mjs is kept as defense-in-depth. Source: codex-rs/hooks/src/events/session_start.rs `select_handlers(..., matcher_input: Some(request.source.as_str()))` and codex-rs/hooks/src/events/common.rs `is_exact_matcher` — "clear" is all-alphanumeric so it's matched as exact equality, not regex. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…essionStart (v0.4.0) Source of truth: examples/codex-memory-plugin/DESIGN.md (added in this commit). Behavioral changes: - SessionStart matcher widens from `clear` to `clear|startup`. Both sources run the same active-window heuristic; `resume` is a hard no-op (still fires on short reconnects). - Heuristic (DESIGN.md §3): count state files (excluding new session_id) within ACTIVE_WINDOW_MS (default 2 min). 0 → noop, 1 → commit it (just-ended session), ≥2 → skip and rely on idle TTL. Tunable via OPENVIKING_CODEX_ACTIVE_WINDOW_MS. - Idle-TTL sweep returns at the tail of session-start-commit.mjs only (not every Stop). Default IDLE_TTL_MS = 30 min via OPENVIKING_CODEX_IDLE_TTL_MS. Catches SIGTERM/Ctrl+C/`/exit` orphans and the ≥2-active skip path. - Stop hook deliberately does NOT sweep — state-write-on-every-turn already gives us the freshness signal. Marker comment added. - Stop hook adds post-compact transcript-shrink defense: if allTurns.length < state.capturedTurnCount, reset capturedTurnCount = 0. - Commit-on-failure preserves state everywhere (PreCompact, heuristic, idle sweep). A non-2xx /commit no longer clears ovSessionId; the next sweep retries. - session-state.mjs saveState now uses atomic write (tmpfile + rename) for crash safety. listStates ignores the brief `<id>.json.tmp` window. Bump: package.json + .codex-plugin/plugin.json → 0.4.0. Docs: README "How It Works" gained a DESIGN.md pointer and rewrites the SessionStart section to reflect heuristic + idle TTL. VERIFICATION.md step 6 now exercises all four heuristic branches (0/1/≥2 active, idle TTL, resume). Phase-2 resume context inject documented in DESIGN.md but explicitly out of scope here. Verified locally with synthetic stdin tests against a fake OV server: 1-active commit, ≥2-active skip, idle TTL sweep, resume noop, unreachable-server keeps state. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses three review points on PR volcengine#1957: 1. Honor OPENVIKING_CLI_CONFIG_FILE for the ovcli.conf override path (matches the convention used by `ov` CLI and claude-code-memory-plugin). OPENVIKING_CONFIG_FILE stays as the ov.conf override; for backward compat it still works when pointed at an ovcli-shaped file. 2. Strict env-first priority for every connection / identity field (baseUrl, apiKey, account, user, agentId). Env vars now win over ovcli.conf, which wins over ov.conf's codex.* block / server.*, which wins over built-in defaults. 3. Unify hook and MCP-server config loading: src/memory-server.ts now imports loadConfig from scripts/config.mjs (relative path stays valid post-compile because servers/ and scripts/ are siblings), eliminating the divergent account/user/agentId fallback chains the PR-Agent reviewer flagged. Auth header: emit Authorization: Bearer (primary, required by OpenViking Cloud) plus the legacy X-API-Key during the transition window. All six fetch sites updated (4 hook scripts + memory-server.ts + compiled servers/memory-server.js). README: document the new resolution chain, OPENVIKING_CLI_CONFIG_FILE, OPENVIKING_BEARER_TOKEN alias, and the Authorization: Bearer migration.
01036e2 to
67c770c
Compare
…CP startup (#2019) * docs(plugin/codex): add dedicated agent-integrations page + fix MCP startup Follow-up to #1957. Lifts Codex out of `04-other-plugins.md` into its own `04-codex.md` (en + zh) with full install steps, configuration, hook behavior, and troubleshooting — mirrors the shape of `02-claude-code.md`. Renumbers `04-other-plugins.md` → `05-` and `05-langchain-langgraph.md` → `06-`. Overview tables in both locales updated; cross-refs fixed. Also fixes two install/runtime bugs surfaced while validating the fresh installer flow against the merged PR: 1. **Stale repo clone**: `setup-helper/install.sh` previously skipped the clone if `~/.openviking/openviking-repo` already existed, so a user who installed before #1957 merged ended up with a pre-PR plugin checkout (no `scripts/`, no `servers/memory-server.js`). The installer now `git fetch + reset --hard` an existing checkout to `$REPO_REF` (default `main`), matching the claude-code installer pattern. 2. **`${CODEX_PLUGIN_ROOT}` not expanded in `.mcp.json`**: Codex 0.130 does not substitute env vars in `.mcp.json` `args`/`env` and does not always inject `CODEX_PLUGIN_ROOT` into MCP child env. The literal string `${CODEX_PLUGIN_ROOT}` was being passed to node, which then tried to resolve `${CODEX_PLUGIN_ROOT}/scripts/start-memory-server.mjs` against codex's cwd and failed with `MODULE_NOT_FOUND`. Fix: - `.mcp.json`: `args: ["scripts/start-memory-server.mjs"]` + `cwd: "."` (matches the syntax 0.1.0 used, which Codex does honor) - `scripts/runtime-common.mjs`: derive plugin root from `import.meta.url` as a fallback so the launcher works regardless of whether `CODEX_PLUGIN_ROOT` is set in the spawn env Bumps plugin to 0.4.1 (package.json + plugin.json + lockfile) since the runtime-common.mjs change invalidates the install-state hash and forces a re-install of node_modules into the per-user runtime data root. * fix(plugin/codex): hooks.json must use relative paths, not ${CODEX_PLUGIN_ROOT} Same root cause as the .mcp.json fix in the previous commit: Codex 0.130 does not expand ${CODEX_PLUGIN_ROOT} in hooks.json `command` strings. The shell that runs the hook sees the literal ${CODEX_PLUGIN_ROOT} and expands it to "" (or leaves it literal), so node tries to load `/scripts/...mjs` and exits 1. Symptom in the chat UI: • SessionStart hook (failed) error: hook exited with code 1 • UserPromptSubmit hook (failed) • Stop hook (failed) Fix: use `./scripts/<name>.mjs` paths, matching the pattern Codex's own bundled plugins (e.g. figma) use. Codex's hook dispatcher resolves these relative to the plugin root (where hooks.json lives). The MCP launcher fix from the prior commit already handles the same class of bug for .mcp.json; this catches the hooks path. * fix(plugin/codex): hooks.json needs absolute paths rendered at install time Previous fix (relative ./scripts/...) was based on the figma example but empirically does not work on Codex 0.130: the hook subprocess runs with cwd = user's cwd (not plugin root) and CODEX_PLUGIN_ROOT is NOT injected into the env. So both ${CODEX_PLUGIN_ROOT}/scripts/foo.mjs and ./scripts/foo.mjs resolve to the wrong absolute path and node exits 1. Verified with a probe shell script wired into hooks.json: argv: /tmp/codex-hook-probe.sh SessionStart cwd: /Users/<user> CODEX_PLUGIN_ROOT: <unset> CODEX_PLUGIN_DATA: <unset> (The "Under-development features are incomplete" banner Codex prints when plugin_hooks is enabled is real - the hook env wiring is unfinished in 0.130.) Fix: keep the source hooks.json as a template (uses __OPENVIKING_PLUGIN_ROOT__ placeholder) and have install.sh sed-render the cache copy with the absolute $CACHE_DIR path on every install. The cached hooks.json is now fully self-contained absolute-path commands; the repo's checked-in copy stays portable. .mcp.json is unaffected: Codex 0.130 does honor the `cwd: "."` field for MCP servers, so relative args resolve against plugin root there. * fix(plugin/codex): bump UserPromptSubmit timeout to 15s Empirically the auto-recall hook can take 0.8s–4s end-to-end (depending on result count and remote OV latency), and Codex 0.130 sometimes adds 4-5s of spawn overhead before our script even starts. The original 8s budget was borderline and produced spurious "hook timed out after 8s" UI errors on slow paths even when the recall would have succeeded. 15s matches the auto-recall internal timeoutMs default (config.mjs:186) and gives enough headroom for spawn-time variance without holding the user's input noticeably longer in the worst case. * fix(plugin/codex): installer accepts OPENVIKING_REPO_BRANCH as alias Per review feedback: the claude-code installer uses OPENVIKING_REPO_BRANCH for the same purpose. Aliasing both names lets users reuse one env var across installers without remembering which plugin uses which name. Precedence: OPENVIKING_REPO_REF > OPENVIKING_REPO_BRANCH > "main".
Summary
Brings
examples/codex-memory-pluginto feature parity withexamples/claude-code-memory-pluginby wiring all four Codex lifecycle hooks. Every Codex session now gets:SessionStart→bootstrap-runtime.mjs—npm ci --omit=devinto${CODEX_PLUGIN_DATA}/runtime(or~/.openviking/codex-memory-plugin/runtime), so MCP runtime deps install once and cache.UserPromptSubmit→auto-recall.mjs— searchesviking://user/memories,viking://agent/memories,viking://agent/skills; ranks with leaf/preference/temporal/lexical boosts; reads top leaves; emitshookSpecificOutput.additionalContext.Stop→auto-capture.mjs— incremental transcript parsing +last_assistant_messagecapture; each capture creates an OV session, posts the text, and calls/api/v1/sessions/{id}/commitso the extraction pipeline lands persistent leaf memories underviking://user/<user>/memories/.PreCompact→pre-compact-capture.mjs— the new piece for Codex. Before Codex compacts the conversation, posts the entire transcript to one OV session and commits, so detail survives summarization.The MCP server (
openviking_recall/_store/_forget/_health) stays available for explicit operations and now reads~/.openviking/ovcli.conf(the canonical client config) so the plugin works against hosted OpenViking deployments out of the box.Codex vs Claude Code differences baked into the scripts
CLAUDE_PLUGIN_ROOTCODEX_PLUGIN_ROOTCLAUDE_PLUGIN_DATACODEX_PLUGIN_DATAUserPromptSubmitinjectiondecision: "approve"+hookSpecificOutput.additionalContexthookSpecificOutput.additionalContextonly —approveis not a Codex outputStopno-opdecision: "approve"{}— onlyblockis a valid CodexdecisionPreCompact(full-transcript commit)claude_codecodex~/.openviking/ov.conf~/.openviking/ovcli.conf, falls back toov.confThese were derived from
codex-rs/hooks/schema/generated/— see the README for the full input/output schema breakdown.What's in the diff
hooks/hooks.json(new)scripts/{auto-recall,auto-capture,pre-compact-capture,bootstrap-runtime,start-memory-server,runtime-common,config,debug-log}.mjs(new)servers/memory-server.js(compiled, checked in — same pattern as claude-code-memory-plugin)src/memory-server.ts(refactored to readovcli.conf+ env-var-only mode).codex-plugin/plugin.json(addshooksandmcpServersfields).mcp.json(uses${CODEX_PLUGIN_ROOT}/scripts/start-memory-server.mjsso the MCP path also benefits from runtime bootstrap)README.md(architecture diagram, full validation SOP, configuration reference, differences table)Test plan
node --checkon everyscripts/*.mjsandtscclean forsrc/memory-server.tsUserPromptSubmitpayload toauto-recall.mjsagainst a hosted OV (https://ov.zaynjarvis.com) — returned 6 ranked leaves and a well-formedhookSpecificOutput.additionalContext.Stoppayload — created OV session5e39a84e-…, committed, and afterov waitthe extraction landedevents/2026/05/10/codex_plugin_smoke_test.mdandpreferences/.../coffee_preference.md.PreCompactpayload — created OV sessionb2954283-…, committed all 4 turns witharchived=true, and the extractor landedentities/software/atlas_river.md,preferences/.../dev_tool_appearance.md, plus matching event memories.ov ls viking://session/<id>→ov ls viking://session/<id>/history(archive_NNN appears) →ov wait→ov find <seed>returns leaf memories underviking://user/<user>/memories/.codex plugin marketplace add /tmp/ov-codex-mp) once[plugins."openviking-memory@<marketplace>"]is enabled in~/.codex/config.toml.🤖 Generated with Claude Code